<script setup lang="tsx">
import {
  NxButton,
  NModal,
  NForm,
  NFormItem,
  NInput,
  NButton,
  NUpload,
  NDataTable,
  NSpin,
  NSpace,
  useMessage,
  NPopconfirm,
  type UploadFileInfo
} from 'naive-ui'
import { nextTick, ref, watch } from 'vue'
import type { FormInst } from 'naive-ui'
import {
  getDocListApi,
  downloadFileApi,
  uploadFileApi,
  renameFileApi,
  mergeFileApi
} from '@/api/file'
import type { FileItemType } from '@/types/api/file'
import dayjs from 'dayjs'
import { deleteFileApi, mutipartUploadApi, uploadPrepare } from '../../api/file'
import { getFileMd5, promisePool } from '@/utils/common'
import jsPreviewPdf from '@js-preview/pdf'
import type { JsPdfPreview } from '@js-preview/pdf'

const messge = useMessage()

const fileSizeLimit = 100 // MB

// 获取文档列表
const loading = ref(false)
const searchParams = ref({
  key: ''
})
const docList = ref<FileItemType[]>([])
const getDocList = async () => {
  loading.value = true
  try {
    const result = await getDocListApi({ key: searchParams.value.key })
    docList.value = result.content.info || result.content.list
  } catch (error) {
    console.error(error)
  }
  loading.value = false
}
getDocList()

// 上传文件
const showModal = ref(false)
const isUploading = ref(false)
const formRef = ref<FormInst | null>(null)
const model = ref<{ filename: string; file: UploadFileInfo[] }>({
  filename: '',
  file: []
})
watch(showModal, (val) => {
  if (!val) {
    model.value = {
      filename: '',
      file: []
    }
  }
})
const rules = {
  file: {
    required: true,
    message: '请选择文件',
    trigger: 'upload',
    validator: (rule: any, value: any) => value.length > 0
  }
}
const onUploadChange = () => {
  Promise.resolve().then(() => {
    formRef.value?.validate(
      () => {},
      (rule) => {
        return rule.trigger === 'upload'
      }
    )
  })
}
// 单步上传
const simpleUpload = async (params: { file: File; filename: string }) => {
  return await uploadFileApi(params)
}
// 分片上传
const mutipartUpload = async (params: { file: File; filename: string }) => {
  const { file, filename } = params
  const chunkSize = 5 * 1024 * 1024
  const fileMd5 = await getFileMd5(file)
  // 发送预检请求
  const prepareResult = await uploadPrepare({
    filename,
    md5: fileMd5
  })
  const { exist, chunks: uploadedChunks } = prepareResult.content
  if (exist) {
    return
  }
  const fileKey = filename.replace(/(.+)(\..+)$/, `$1.${fileMd5}$2`)
  const chunks = []
  const chunkCount = Math.ceil(file.size / chunkSize)
  if (chunkCount === uploadedChunks.length) {
    // 发送合并请求
    return await mergeFileApi({ filename, md5: fileMd5 })
  }
  for (let i = 1; i <= chunkCount; i++) {
    if (uploadedChunks.includes(i)) {
      continue
    }
    const chunkArrayBuffer = file.slice((i - 1) * chunkSize, i * chunkSize)
    const chunkMd5 = await getFileMd5(chunkArrayBuffer)
    chunks.push({
      filename: fileKey,
      chunkIndex: i,
      arrayBuffer: chunkArrayBuffer,
      md5: chunkMd5
    })
  }
  const pool = chunks.map((chunk) => {
    return async () =>
      mutipartUploadApi({
        file: chunk.arrayBuffer,
        filename: chunk.filename + '_chunks',
        chunkIndex: chunk.chunkIndex.toString(),
        md5: chunk.md5
      }).then(async (res) => {
        if (res.content.count === chunkCount) {
          // 发送合并请求
          return await mergeFileApi({ filename, md5: fileMd5 })
        }
      })
  })
  return await promisePool(pool, 5)
}

const save = async () => {
  isUploading.value = true
  try {
    await formRef.value?.validate()
    const file = model.value.file[0].file as File
    const extension = file.name.match(/[^.]+$/)?.[0] || ''
    const filename = model.value.filename ? `${model.value.filename}.${extension}` : file.name
    const params = {
      file,
      filename
    }
    if (params.file.size < 20 * 1024 ** 2) {
      await simpleUpload(params) // 直接上传
    } else {
      await mutipartUpload(params) // 分片上传
    }
    showModal.value = false
    getDocList()
    messge.success('上传成功')
  } catch (error) {
    console.error(error)
  }
  isUploading.value = false
}

// 表头
const columns = [
  {
    title: '文件唯一标识',
    key: 'fs_id'
  },
  {
    title: '文件名',
    key: 'server_filename',
    render(row: FileItemType) {
      const oldName = row['server_filename']
      if (row.isEdit) {
        return (
          <NInput
            defaultValue={oldName}
            class={`rename_input_${row.fs_id}`}
            input-props={{ spellcheck: false }}
            onChange={(v: string) => renameFile(row, v)}
          />
        )
      } else {
        return oldName
      }
    }
  },
  {
    title: '文件大小',
    key: 'size',
    render(row: FileItemType) {
      return `${(row.size / 1024 ** 2).toFixed(2)}MB`
    }
  },
  {
    title: '创建时间',
    key: 'local_ctime',
    render(row: FileItemType) {
      return dayjs(row.local_ctime * 1000).format('YYYY-MM-DD HH:mm:ss')
    }
  },
  {
    title: '操作',
    key: '',
    width: 200,
    render(row: FileItemType) {
      return (
        <NSpace>
          <NButton text tag="a" onClick={() => download(row)}>
            下载
          </NButton>
          <NButton
            text
            tag="a"
            onClick={() => {
              row.isEdit = true
              setTimeout(() => {
                const className = `rename_input_${row.fs_id}`
                const input = document.querySelector<HTMLInputElement>(`.${className} input`)
                input?.focus()
              }, 0)
            }}
          >
            重命名
          </NButton>
          <NButton text tag="a" onClick={() => preview(row)}>
            预览
          </NButton>
          <NPopconfirm
            onPositiveClick={() => deleteFile(row)}
            positiveButtonProps={{ loading: deleteLoading.value }}
            v-slots={{
              trigger: () => (
                <NButton text tag="a" type="error">
                  删除
                </NButton>
              )
            }}
          >
            确认要删除吗?
          </NPopconfirm>
        </NSpace>
      )
    }
  }
]

// 下载
const download = async (record: FileItemType) => {
  if (record.size >= fileSizeLimit * 1024 ** 2) {
    return messge.warning(`超过${fileSizeLimit}M的文件需要到百度网盘客户端下载`)
  }
  loading.value = true
  try {
    await downloadFileApi({
      fsId: record.fs_id
    })
  } catch (error) {
    console.error(error)
  }
  loading.value = false
}

// 重命名
const renameFile = async (record: FileItemType, newName: string) => {
  const params = {
    path: record.path,
    newName
  }
  loading.value = true
  try {
    await renameFileApi(params)
    getDocList()
    messge.success('重命名成功')
  } catch (error) {
    console.log(error)
  }

  loading.value = false
}

// 删除
const deleteLoading = ref(false)
const deleteFile = async (record: FileItemType) => {
  const params = {
    path: record.path
  }
  deleteLoading.value = true
  try {
    await deleteFileApi(params)
    getDocList()
    messge.success('删除成功')
  } catch (error) {
    console.error(error)
  }
  deleteLoading.value = false
}

// 预览
const showPreviewModal = ref(false)
const fileBlob = ref<Blob>()
const preview = async (record: FileItemType) => {
  if (record.size >= fileSizeLimit * 1024 ** 2) {
    return messge.warning(`超过${fileSizeLimit}M的文件需要到百度网盘客户端预览`)
  }
  loading.value = true
  try {
    const blob = await downloadFileApi(
      {
        fsId: record.fs_id
      },
      true
    )
    fileBlob.value = blob
    showPreviewModal.value = true
  } catch (error) {
    console.error(error)
  }
  loading.value = false
}
watch(showPreviewModal, (val) => {
  let container: HTMLDivElement | null = null
  let myPdfPreviewer = null
  if (val) {
    nextTick(() => {
      container = document.querySelector('.preview-container')
      if (container) {
        myPdfPreviewer = jsPreviewPdf.init(container)
        fileBlob.value &&
          myPdfPreviewer
            .preview(fileBlob.value)
            .then(() => {
              console.log('预览完成')
            })
            .catch((e) => {
              console.log('预览失败', e)
            })
      }
    })
  } else {
    myPdfPreviewer && (myPdfPreviewer as JsPdfPreview).destroy()
    container = null
  }
})
</script>

<template>
  <div class="home-container flex-col">
    <div class="operate-container flex align-items-center justify-content-between">
      <div>
        <NForm label-placement="left" inline>
          <n-form-item label="文件名:">
            <n-input
              placeholder="请输入文件名"
              v-model:value="searchParams.key"
              clearable
              @keydown.enter="getDocList"
            />
          </n-form-item>
          <NButton type="primary" @click="getDocList">查询</NButton>
        </NForm>
      </div>
      <div>
        <NSpace>
          <NxButton type="primary" @click="() => (showModal = true)">上传</NxButton>
        </NSpace>
      </div>
    </div>
    <div class="table-container">
      <NSpin :show="loading">
        <NDataTable :columns="columns" :data="docList" striped />
      </NSpin>
    </div>
    <!-- 上传弹窗 -->
    <NModal v-model:show="showModal" preset="card" :style="{ width: '600px' }" title="上传文件">
      <NForm ref="formRef" :model="model" :rules="rules">
        <NFormItem label="文件名" path="filename">
          <NInput
            placeholder="请输入文件名(不带.后缀)"
            maxlength="20"
            v-model:value="model.filename"
            :input-props="{ spellcheck: false }"
          ></NInput>
        </NFormItem>
        <NFormItem label="上传文件" path="file">
          <NUpload
            :default-upload="false"
            accept=".pdf,.doc"
            v-model:file-list="model.file"
            :show-trigger="model.file.length === 0"
            :on-change="onUploadChange"
          >
            <NxButton>选择文件</NxButton>
          </NUpload>
        </NFormItem>
      </NForm>
      <template #footer>
        <div class="flex justify-content-end">
          <NButton :style="{ marginRight: '10px' }" @click="() => (showModal = false)">
            取消
          </NButton>
          <NButton type="primary" @click="save" :loading="isUploading">保存</NButton>
        </div>
      </template>
    </NModal>
    <!-- 预览弹窗 -->
    <NModal v-model:show="showPreviewModal" preset="card" :style="{ width: '75vw' }" title="预览">
      <div
        class="preview-container"
        style="max-height: 80vh; overflow: auto; margin-right: -16px; padding-right: 10px"
      ></div>
    </NModal>
  </div>
</template>

<style scoped lang="scss">
.home-container {
  overflow-x: hidden;
  height: 100%;
  .operate-container {
    height: 70px;
    background-color: #fff;
    border-radius: 4px;
    padding: 16px;
    :deep(.n-form-item-feedback-wrapper) {
      display: none !important;
    }
  }
  .table-container {
    margin-top: 16px;
    background: #fff;
    flex: 1;
  }
}
</style>
